package cn.stylefeng.roses.kernel.ca.server.core.sso;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.stylefeng.roses.kernel.auth.api.TenantCodeGetApi;
import cn.stylefeng.roses.kernel.ca.api.*;
import cn.stylefeng.roses.kernel.ca.api.business.CaValidatePasswordApi;
import cn.stylefeng.roses.kernel.ca.api.context.CaLoginContext;
import cn.stylefeng.roses.kernel.ca.api.exception.CaServerException;
import cn.stylefeng.roses.kernel.ca.api.exception.enums.CaServerExceptionEnum;
import cn.stylefeng.roses.kernel.ca.api.pojo.CaClientInfo;
import cn.stylefeng.roses.kernel.ca.api.pojo.SsoTokenBuild;
import cn.stylefeng.roses.kernel.ca.api.pojo.sso.CaLoginUser;
import cn.stylefeng.roses.kernel.ca.api.pojo.sso.request.SsoDetectionRequest;
import cn.stylefeng.roses.kernel.ca.api.pojo.sso.request.SsoLoginCodeRequest;
import cn.stylefeng.roses.kernel.ca.api.pojo.sso.request.SsoLoginRequest;
import cn.stylefeng.roses.kernel.ca.api.pojo.sso.request.SsoLogoutRequest;
import cn.stylefeng.roses.kernel.ca.server.core.threadlocal.CaClientHolder;
import cn.stylefeng.roses.kernel.ca.server.core.token.CaLoginUserEncryptApiFactory;
import cn.stylefeng.roses.kernel.ca.server.modular.manage.entity.SsoClient;
import cn.stylefeng.roses.kernel.ca.server.modular.manage.pojo.request.SsoClientRequest;
import cn.stylefeng.roses.kernel.ca.server.modular.manage.service.SsoClientService;
import cn.stylefeng.roses.kernel.db.mp.tenant.holder.TenantIdHolder;
import cn.stylefeng.roses.kernel.sys.api.SysUserServiceApi;
import cn.stylefeng.roses.kernel.sys.api.pojo.user.UserInfoDetailDTO;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Date;

/**
 * 单点登录的业务实现
 *
 * @author fengshuonan
 * @date 2021/1/21 14:52
 */
@Service
public class SsoServiceImpl implements SsoService, SsoLoginCodeServiceApi {

    @Resource
    private SsoClientService ssoClientService;

    @Resource
    private CaSessionManagerApi caSessionManagerApi;

    @Resource
    private RedirectUrlCreatorApi redirectUrlCreatorApi;

    @Resource
    private CaValidatePasswordApi caValidatePasswordApi;

    @Resource
    private CaClientTokenApi caClientTokenApi;

    @Resource
    private SysUserServiceApi sysUserServiceApi;

    @Resource
    private TenantCodeGetApi tenantCodeGetApi;

    @Override
    public String detection(SsoDetectionRequest ssoDetectionRequest) {
        try {
            // 1. 根据clientId获取客户端应用信息
            SsoClientRequest ssoClientRequest = new SsoClientRequest();
            ssoClientRequest.setClientId(ssoDetectionRequest.getClientId());
            SsoClient clientInfo = ssoClientService.detail(ssoClientRequest);
            CaClientHolder.set(clientInfo);

            // 2. 获取当前的CA登录用户
            // 注意：这一步如果获取不到统一认证中心的会话
            // 可能会抛出：CaServerExceptionEnum.COOKIE_IS_NULL和CaServerExceptionEnum.CA_SESSION_EXPIRED
            // 异常由控制器层捕获到
            CaLoginUser caloginUser = CaLoginContext.me().getLoginUser();

            // 3. 创建ca的会话并进行跳转到业务端操作
            return createCaSessionAndRedirect(ssoDetectionRequest.getSsoCallback(), clientInfo, caloginUser, false);

        } catch (CaServerException caServerException) {
            // 错误码拼接RedirectUrl返回给业务应用
            return this.ssoLoginError(caServerException, ssoDetectionRequest.getSsoCallback());
        } finally {
            CaClientHolder.remove();
        }
    }

    @Override
    public String createSsoLoginCode(SsoLoginCodeRequest ssoLoginCodeRequest) {

        // 通过租户编码获取租户id，如果租户参数没传，则默认填充根租户的id
        String tenantCode = ssoLoginCodeRequest.getTenantCode();
        Long tenantId = tenantCodeGetApi.getTenantIdByCode(tenantCode);

        // 1. 校验账号密码是否正确，获取用户的基本信息
        Long userId;
        try {
            TenantIdHolder.set(tenantId);
            userId = caValidatePasswordApi.validatePassword(ssoLoginCodeRequest);
        } finally {
            TenantIdHolder.remove();
        }

        // 2. 创建ssoLoginCode
        String ssoLoginCode = IdWorker.get32UUID();

        // 3. 暂存ssoLoginCode
        CaLoginUser caLoginUser = new CaLoginUser();

        // 4. 存储用户id和生成单点会话的token，ca token用随机字符串即可
        caLoginUser.setUserId(userId);
        caLoginUser.setCaToken(IdWorker.getIdStr());
        caLoginUser.setTenantId(tenantId);
        CaLoginContext.me().stashSsoLoginCode(ssoLoginCode, caLoginUser);

        return ssoLoginCode;
    }

    @Override
    public String activateByLoginCode(SsoLoginRequest ssoLoginRequest) {

        // 1. 根据clientId获取客户端应用信息
        SsoClientRequest ssoClientRequest = new SsoClientRequest();
        ssoClientRequest.setClientId(ssoLoginRequest.getClientId());
        SsoClient clientInfo = ssoClientService.detail(ssoClientRequest);
        CaClientHolder.set(clientInfo);

        // 2. 根据ssoLoginCode获取用户信息
        // 这时的用户信息里，只有一个token和用户id
        String ssoLoginCode = ssoLoginRequest.getSsoLoginCode();
        CaLoginUser caLoginUser = CaLoginContext.me().destroySsoLoginCode(ssoLoginCode);

        // 2.1 补充用户信息
        try {
            TenantIdHolder.set(caLoginUser.getTenantId());
            UserInfoDetailDTO userDetail = sysUserServiceApi.getUserDetail(caLoginUser.getUserId());
            caLoginUser.setAccount(userDetail.getAccount());
            caLoginUser.setRealName(userDetail.getRealName());
        } finally {
            TenantIdHolder.remove();
        }

        // 3. 根据用户信息生成token，跳转到业务端使用
        return createCaSessionAndRedirect(ssoLoginRequest.getSsoCallback(), clientInfo, caLoginUser, true);
    }

    @Override
    public String ssoLogout(SsoLogoutRequest ssoLogoutRequest) {
        try {
            return executeSsoLogoutAction(ssoLogoutRequest);
        } catch (CaServerException caServerException) {
            return this.ssoLogoutError(caServerException);
        } finally {
            CaClientHolder.remove();
        }
    }

    /**
     * 创建统一认证中心的会话，并进行跳转到业务端
     *
     * @author fengshuonan
     * @since 2023/11/5 15:10
     */
    private String createCaSessionAndRedirect(String ssoCallback, SsoClient clientInfo, CaLoginUser caloginUser,
                                              boolean firstCreateSession) {
        // 3. 如果获取到了CA 统一认证中心的会话，则开始进行装配token
        // token是由JSON构成，包含了用户相关的信息，并经过AES对称加密返回Redirect给客户端URL
        String encryptUserInfoToken = this.generateBusinessToken(clientInfo.getCaTokenSecret(), caloginUser);

        // 4. 创建或刷新当前用户，在统一认证中心的的sso session
        String caToken = caloginUser.getCaToken();
        if (firstCreateSession) {
            caSessionManagerApi.createCaSessionAndCookie(caToken, caloginUser);
        } else {
            caSessionManagerApi.refreshSession(caToken);
        }

        // 5. 将用用户针对登录过的业务应用，进行一个缓存
        CaClientInfo tempClientInfo = new CaClientInfo();
        BeanUtil.copyProperties(clientInfo, tempClientInfo);
        caClientTokenApi.addClientToken(caToken, tempClientInfo);

        // 5. 组装redirect到客户端应用的url
        return redirectUrlCreatorApi.createClientSsoCallbackUrl(clientInfo.getClientId(), encryptUserInfoToken, ssoCallback,
                clientInfo.getSsoCallbackUrl());
    }

    /**
     * 生成跳转到业务端的Token
     *
     * @author fengshuonan
     * @since 2023/11/5 15:03
     */
    private String generateBusinessToken(String clientSecret, CaLoginUser caLoginUser) {

        // 1，则开始进行装配token，token是由JSON构成，包含了用户相关的信息，并经过AES对称加密返回Redirect给客户端URL
        CaLoginUserEncryptApi encryptApi = CaLoginUserEncryptApiFactory.createEncryptApi(clientSecret);

        // 2 生成当前的时间戳，共同放进json中
        // 这个时间戳给客户端校验token是否过期
        String currentDateTime = DateUtil.format(new Date(), "yyyyMMddHHmmss");
        SsoTokenBuild ssoTokenBuild = new SsoTokenBuild(caLoginUser.getCaToken(), caLoginUser, currentDateTime);
        return encryptApi.encrypt(ssoTokenBuild);
    }

    /**
     * SSO认证检测（异常流程）
     *
     * @param caServerException  异常流程原因
     * @param requestSsoCallback 请求中携带的callbackUrl
     * @return 单点之后的url
     * @author fengshuonan
     * @date 2021/1/22 10:55
     */
    private String ssoLoginError(CaServerException caServerException, String requestSsoCallback) {

        // 获取错误码
        String errorCode = caServerException.getErrorCode();

        // client信息不存在
        if (CaServerExceptionEnum.CLIENT_NOT_EXIST.getErrorCode().equals(errorCode)) {
            if (StrUtil.isNotBlank(requestSsoCallback)) {
                return redirectUrlCreatorApi.createClientSsoCallbackErrorUrl(requestSsoCallback, errorCode);
            }
        }

        // Cookie获取的用户信息为空，或CA的会话过期，跳转到登录界面
        if (CaServerExceptionEnum.COOKIE_IS_NULL.getErrorCode().equals(errorCode) || CaServerExceptionEnum.CA_SESSION_EXPIRED.getErrorCode()
                .equals(errorCode)) {
            Integer ssoLoginUrlType = CaClientHolder.get().getLoginPageType();
            return redirectUrlCreatorApi.createLoginPageUrl(ssoLoginUrlType, CaClientHolder.get().getCustomLoginUrl(), errorCode);
        }

        // 登录失败，loginCode对应用户信息获取不到，跳转到登录界面
        if (CaServerExceptionEnum.LOGIN_CODE_USER_IS_NULL.getErrorCode().equals(errorCode)) {
            Integer ssoLoginUrlType = CaClientHolder.get().getLoginPageType();
            return redirectUrlCreatorApi.createLoginPageUrl(ssoLoginUrlType, CaClientHolder.get().getCustomLoginUrl(), errorCode);
        }

        throw caServerException;
    }

    /**
     * 单点登录退出的逻辑
     *
     * @author fengshuonan
     * @date 2021/2/1 15:45
     */
    private String executeSsoLogoutAction(SsoLogoutRequest ssoLogoutRequest) {

        // 1. 根据clientId获取客户端应用信息
        SsoClientRequest ssoClientRequest = new SsoClientRequest();
        ssoClientRequest.setClientId(ssoLogoutRequest.getClientId());
        SsoClient clientInfo = ssoClientService.detail(ssoClientRequest);

        // 1.1 添加线程变量，在catch到异常时，会调用这个变量信息
        CaClientHolder.set(clientInfo);

        // 2. 获取当前登录的sso用户
        CaLoginUser loginUser = CaLoginContext.me().getLoginUser();

        // 3. 销毁当前登录用户的会话
        caSessionManagerApi.destroySession(loginUser.getCaToken());

        // 3.1 清除所有其他本用户登录的业务系统的session
        caClientTokenApi.logoutClientTokens(loginUser.getCaToken());

        // 4. 组装登录界面的redirect地址
        return redirectUrlCreatorApi.createLoginPageUrl(clientInfo.getLoginPageType(), clientInfo.getCustomLoginUrl(),
                CaServerExceptionEnum.SUCCESS_LOGOUT.getErrorCode());
    }

    /**
     * SSO退出失败的处理
     *
     * @author fengshuonan
     * @date 2021/2/1 15:46
     */
    private String ssoLogoutError(CaServerException caServerException) {

        // 获取错误码
        String errorCode = caServerException.getErrorCode();

        // 如果clientId为空
        if (CaServerExceptionEnum.CLIENT_IS_NULL.getErrorCode().equals(errorCode)) {
            return redirectUrlCreatorApi.createErrorTipUrl(errorCode);
        }

        // client信息不存在
        if (CaServerExceptionEnum.CLIENT_NOT_EXIST.getErrorCode().equals(errorCode)) {
            return redirectUrlCreatorApi.createErrorTipUrl(errorCode);
        }

        // Cookie获取的用户信息为空，或CA的会话过期，跳转到登录界面
        if (CaServerExceptionEnum.COOKIE_IS_NULL.getErrorCode().equals(errorCode) || CaServerExceptionEnum.CA_SESSION_EXPIRED.getErrorCode()
                .equals(errorCode)) {
            Integer loginPageType = CaClientHolder.get().getLoginPageType();
            return redirectUrlCreatorApi.createLoginPageUrl(loginPageType, CaClientHolder.get().getCustomLoginUrl(),
                    caServerException.getErrorCode());
        }

        throw caServerException;
    }

}
